﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;

using Microsoft.Practices.Composite.Events;

using Matchingo.Common.Types.EventArguments;
using Matchingo.Common.Types.Events;
using Matchingo.Common.Types.Extensions;
using Matchingo.Common.Types.Interfaces;
using Matchingo.Common.Types.Types;

using System.Windows.Browser;

namespace Matchingo.Models.GameLogicProvider
{
    public class MemoryGameLogic : IMemoryGameLogic
    {   
        #region ========== Private Properties ==========

        private IHighScoreProvider ScoreProvider { get; set; }
        private IEventAggregator EventAggregator { get; set; }
        
        #endregion

        #region ========== Public Properties ==========

        private int _gridColumns;
        public int GridColumns 
        {
            get { return _gridColumns; }
            private set
            {
                _gridColumns = value;
                NotifyPropertyChanged("GridColumns");
            }
        }

        private int _gridRows;
        public int GridRows 
        {
            get { return _gridRows; }
            private set
            {
                _gridRows = value;
                NotifyPropertyChanged("GridRows");
            }
        }

        private Game _currentGame;
        public Game CurrentGame 
        {
            get { return _currentGame; }
            private set
            {
                _currentGame = value;
                NotifyPropertyChanged("CurrentGame");
            }
        }

        private int _matchComboCount;
        public int MatchComboCount 
        {
            get { return _matchComboCount; }
            private set
            {
                _matchComboCount = value;
                if (MatchComboCountChanged != null)
                {
                    MatchComboCountChanged(this, EventArgs.Empty);
                }
            }
        }

        #endregion

        #region ========== Constructors ==========

        public MemoryGameLogic(IHighScoreProvider scoreProvider, IEventAggregator eventAggregator)
        {            
            CurrentGame = null;

            ScoreProvider = scoreProvider;
            EventAggregator = eventAggregator;
        }

        #endregion

        #region ========== Public Methods ==========

        public void Initialize()
        {
        }

        public void StartNewGame(MatchGameSettings settings)
        {
            StartNewGame(settings, null);
        }

        public void StartNewGame(MatchGameSettings settings, ObservableCollection<Card> replayCards)
        {
            CurrentGame = new Game() {  
                                        ArtificialGame = settings.AIGame, 
                                        CardCount = settings.CardCount, 
                                        PlayerName = settings.PlayerName,
                                        ReplayGame = !(replayCards == null || replayCards.Count == 0)
                                    };
            UpdateScore();

            if (replayCards == null || replayCards.Count == 0)
            {
                CurrentGame.CardsSet = new ObservableCollection<Card>();

                int uniqueCardPairs = CurrentGame.CardCount / 2; // 2 cards make up a unique pair

                // iterate through all the unique card pairs, and build up a card set
                for (int ix = 0; ix < uniqueCardPairs; ix++)
                {
                    // add two copies of this card to allow for matching, we will shuffle the cards later
                    Card c1 = new Card();
                    Card c2 = new Card(c1); // create a 2nd copy with the same MatchingID, but a different UniqueID
                    CurrentGame.CardsSet.Add(c1);
                    CurrentGame.CardsSet.Add(c2);
                }

                // shuffle the set of cards
                // TODO:: this looks bad, cant seem to modify params passed in via 'this' in extension methods?
                CurrentGame.CardsSet = CurrentGame.CardsSet.Shuffle(); // TODO:: this looks bad, cant seem to modify params passed in via 'this' in extension methods?
            }
            else
            {
                CurrentGame.CardsSet = replayCards;
            }

            NotifyPropertyChanged("GameCardSet"); // TODO:: seems odd to be doing this here

            // TODO:: simplify
            int tempColumnCount, tempRowCount;
            GetGridSize(CurrentGame.CardCount, out tempColumnCount, out tempRowCount);
            GridColumns = tempColumnCount;
            GridRows = tempRowCount;
            
            CurrentGame.GameState = Game.GameStateEnum.Started;
            MatchComboCount = 0;

			string fbuserid;
			HtmlPage.Document.QueryString.TryGetValue("fb_sig_user", out fbuserid);
			CurrentGame.FBUserID = fbuserid;

            EventAggregator.GetEvent<GameStartedEvent>().Publish(CurrentGame);
        }

        public bool Flip(Guid uniqueCardID)
        {
			bool flipped = false;
            Card c; // will be populated amongst sanity checks
            c = CurrentGame.CardsSet.GetCard(uniqueCardID);

            #region Sanity Checks

            // make sure we could get a handle on the card
            if (c == null)
            {
                throw new ArgumentException("Unable to get a handle on the requested card from GameCardSet");
            }

            // make sure this card hasn't already been matched (if so then they can't flip)
            if (c.IsMatched)
            {
                return false;
            }

            // if the only card flipped up is the card requested to flip then return (you cant flip the same card twice)            
            if (CurrentGame.CardsSet.GetCardsFaceUpNotMatched().Count == 1 &&
                CurrentGame.CardsSet.GetCardsFaceUpNotMatched()[0].UniqueID == uniqueCardID)
            {
                return false;
            }

            #endregion

            // if there are already 2 cards showing (and the one clicked isnt one of them) their front then flip them back over
            if (CurrentGame.CardsSet.GetCardsFaceUpNotMatched().Count == 2 && c.VisibleSide != Card.CardSide.Front)
            {
                // anytime 2 cards get flipped back to rear a turn has ended
                CurrentGame.TurnCount++;

                CurrentGame.CardsSet.FlipAllCardsNotMatchedToRear();
            }

            // flip the card and fire the event
            if (c.VisibleSide != Card.CardSide.Front)
            {
				// flip the card, fire off the event (used by sound engine)
                c.Flip();
				flipped = true;

                // if this was the first time a card was flipped then we need to set the start time on the game
                if (CurrentGame.StartTime == null)
                {
                    CurrentGame.StartTime = DateTime.Now;
                }
            }

            // if there are now 2 cards showing then we need to run our match logic and see if any matches occurred
            if (CurrentGame.CardsSet.GetCardsFaceUpNotMatched().Count == 2)
                DoMatchingLogic();

			return flipped;
        }

        #endregion

        #region ========== Private Methods ==========

        /// <summary>
        /// The grid size should be the closest square root rounded up. 64 cards would be 8x8, 62 would 
        /// also be 8x8 as well with the last row only having 6 cards. 
        /// </summary>
        /// <param name="cardSetSize">The size of the set (or deck) of cards</param>
        /// <param name="cols">How many columns should be in this grid of cards</param>
        /// <param name="rows">How many rows should be in this grid of cards</param>
        private static void GetGridSize(int cardSetSize, out int cols, out int rows)
        {
            #region Sanity Checks

            if (cardSetSize < 2)
                throw new ArgumentException("MatchGameLogic.GetGridSize() - cannot generate grid size for less than 2 cards");

            if (cardSetSize % 2 != 0)
                throw new ArgumentException("MatchGameLogic.GetGridSize() - all card set sizes in a matching game should be even to allow for matching");

            #endregion

            double root = Math.Sqrt(Convert.ToDouble(cardSetSize));
            double roundedUp = Math.Ceiling(root);
            cols = Convert.ToInt32(roundedUp);

            // now we need to see if there are so few cards that there should be less rows than columns
            // ex: with 2 cards you should have 2 columns and 1 row, with 6 cards - 3 columns and 2 rows
            int closestSquare = cols * cols;

            // if it is an even multiple then the rows is the same as the cols .. 
            if (closestSquare == cardSetSize)
            {
                rows = cols;
            }
            else
            {
                // .. otherwise the number of rows will be the closest division of the columns rounded up
                double rowCount = Convert.ToDouble(cardSetSize) / Convert.ToDouble(cols);
                rows = Convert.ToInt32(Math.Ceiling(rowCount));
            }
        }


        private void UpdateScore()
        {
            // if there is no current game then there is nothing for us to read from or to update, return
            if (CurrentGame == null) 
                return;

            int originalScore = CurrentGame.Score;

            // if there have been no moves yet then the score is 0, this occurs when a new game starts
            if (CurrentGame.TurnCount == 0)
            {
                CurrentGame.Score = 0;
            }
            else
            {
                // sanity check - we can't do a calc of the score if there is no card set in play
                if (CurrentGame.CardsSet == null)
                    throw new InvalidProgramException("UpdateScore() was called while there was no game card set in play");

                // TODO:: break into calculation method / class

                // the score is equal to the number of matches over the number of turns (min 0, max 1) times 100 (to get (1-100)
                // multiplied by how far they are through the match (matched / total to be matched ) then multiply by the number 
                // of cards the game was played with
                int matchCount = CurrentGame.CardsSet.GetCardsMatched().Count / 2;
                int possibleMatches = CurrentGame.CardsSet.Count / 2;
                double score = (matchCount / (double)CurrentGame.TurnCount) * 1000.00D *
                    (matchCount / (double)possibleMatches) * CurrentGame.CardsSet.Count;

                // the score is multiplied by how many combos they got (and how big they were)
                foreach (int comboItem in CurrentGame.ComboHistory)
                {
                    if (comboItem == 1)
                        continue;

                    // a score of 10,000 with a 6x multiplier would be = 10,000 * 2 * 3 * 4 * 5 * 6 = 7,200,000 - which is way too
                    // high, so we give only 33% of the multiplyer
                    double multiplyer = 1.0D + (comboItem / 3.00D);
                    score = score * multiplyer;
                }

                CurrentGame.Score = Convert.ToInt32(Math.Ceiling(score));
            }
        }

        private void DoMatchingLogic()
        {
            List<Card> cards = CurrentGame.CardsSet.GetCardsFaceUpNotMatched();

            // make sure that 2 are showing face up, if not then there is no match to analyze
            if (cards.Count != 2)
                throw new InvalidProgramException("Matchingo.DoMatchingLogic() called when 2 cards aren't face up");

            // if the cards match then do what we need to do when a match occurs
            if (cards[0].MatchingID == cards[1].MatchingID)
            {
                // increment the combo count ..  
                MatchComboCount++;

                // a turn has ended, increment the turn count
                CurrentGame.TurnCount++;

                // fire off the matched events
                StartMatchedEvent(cards[0], cards[1]);

                // any time we match a card we need to see if the game should end
                CheckForGameEnded();
            }
            else
            {
                // a match didnt occur, so add the previous combo count to the history
                if (MatchComboCount != 0 && MatchComboCount != 1)
                {
                    CurrentGame.ComboHistory.Add(MatchComboCount);
                }

                MatchComboCount = 0;
            }

            // update the score
            UpdateScore();
        }

        private void StartMatchedEvent(Card c1, Card c2)
        {
            // when a match occurs we need to set them as matched
            c1.FoundMatch(c2);
            c2.FoundMatch(c1);

            // a match occurred, build the event args and fire the event (if anyone is listening)				
            if (CardsMatched != null)
            {
                MatchEventArgs args = new MatchEventArgs(c1, c2);
                CardsMatched(this, args);
            }

            // fire off the ComboOccurred event and pass in the number of items in the ongoing combo (if its more than 1 in the combo)
            if (MatchComboCount > 1)
            {
                EventAggregator.GetEvent<ComboOccurredEvent>().Publish(MatchComboCount);
            }
        }

        private void CheckForGameEnded()
        {
            // see if all the cards are matched
            if (CurrentGame.CardsSet.GetCardsNotMatched().Count == 0)
            {
                // the game has ended
                EndGame();
            }
        }

        private void EndGame()
        {
            // the game ended, so save off this combo to their history
            if (MatchComboCount != 0 && MatchComboCount != 1)
            {
                CurrentGame.ComboHistory.Add(MatchComboCount);
            }

            UpdateScore();

            // update the game state, set the endtime, etc.
            CurrentGame.EndTime = DateTime.Now;
            CurrentGame.GameState = Game.GameStateEnum.Ended;

            // add the game to the highscore list (only if this was a real game)
            if (!CurrentGame.ArtificialGame)
            {
                ScoreProvider.AddRecord(CurrentGame, true);                
            }

            GameEndedEventArgs args = new GameEndedEventArgs(CurrentGame);
            EventAggregator.GetEvent<GameEndedEvent>().Publish(args);
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged == null)
                return;

            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region ========== Public Events ==========

        public event PropertyChangedEventHandler PropertyChanged;

        public event EventHandler<MatchEventArgs> CardsMatched;

        public event EventHandler MatchComboCountChanged;

        #endregion
    }
}
